﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.ServiceModel.Web;
using System.Text;
using System.Data.Objects;

namespace KnihotocWcfService
{
    // NOTE: You can use the "Rename" command on the "Refactor" menu to change the class name "BookCachingService" in code, svc and config file together.
    public class BookCachingService : IBookCachingService
    {
        // converts approximately meters to degrees
        private const double LAT_M_DEG = 0.0001 / 11;
        private const double LON_M_DEG = 0.0001 / 6;
        private const double EARTH_RADIUS = 6378000;

        double deg_to_rad(double deg)
        {
            return deg / (180 / Math.PI);
        }

        /// <summary>
        /// Gets circle distance from 2 coordinates in meters
        /// </summary>
        /// <param name="lat1"></param>
        /// <param name="long1"></param>
        /// <param name="lat2"></param>
        /// <param name="long2"></param>
        /// <returns></returns>
        public double CalculateGreatCircleDistance(double lat1, double long1, double lat2, double long2)
        {
            return EARTH_RADIUS * Math.Acos(
                Math.Sin(lat1) * Math.Sin(lat2)
                + Math.Cos(lat1) * Math.Cos(lat2) * Math.Cos(long2 - long1));
        }

        /// <summary>
        /// Wrapper for CalculateGreatCircleDistance working with degrees
        /// </summary>
        /// <param name="lat1"></param>
        /// <param name="long1"></param>
        /// <param name="lat2"></param>
        /// <param name="long2"></param>
        /// <returns></returns>
        public double CalculateGreatCircleDistanceDeg(double lat1, double long1, double lat2, double long2)
        {
            return CalculateGreatCircleDistance(deg_to_rad(lat1), deg_to_rad(long1), deg_to_rad(lat2), deg_to_rad(long2));
        }

        #region User
        public void AddUser(string fullName, string userName, string password)
        {
            /*
            using (var context = new BOOKCACHINGEntities())
            {
                context.AddToUser(new User()
                {
                    UserName = userName,
                    Password = password,
                    FullName = fullName
                });
                context.SaveChanges();
            }
            */
        }

        public User LoginUser(string username, string password)
        {
            using (var context = new Bookcaching_devEntities1())
            {
                context.ContextOptions.LazyLoadingEnabled = false;
                return context.User.Where(u => u.UserName == username && u.Password == password).DefaultIfEmpty(null).First();
            }

        }

        public User GetUserById(int userId)
        {
            using (var context = new Bookcaching_devEntities1())
            {
                context.ContextOptions.LazyLoadingEnabled = false;
                return context.User.Where(u => u.ID == userId).DefaultIfEmpty(null).First();
            }
        }
        #endregion

        #region Book
        /*
        List<string> IBookCachingService.GetBookNameByID(int id)
        {

            string query = @"SELECT value Book.Title FROM Bookcaching_devEntities1.Book as Book";
            ObjectParameter[] parameters = new ObjectParameter[1];
            parameters[0] = new ObjectParameter("username", "aaa");

            
            using (var context = new Bookcaching_devEntities1())
            {
                return context.Book.Select(b => b.BookState.ToString()).ToList();
                //ObjectQuery<string> results = context.CreateQuery<string>(query, parameters);
                //if (results != null)
                //{
                //    return results.ToList();
                //}
            }

            //return null;

            //return new List<string>() { "aaa", "bbb", "ccc" };
        }
         * */

        public Book GetBookByID(int id)
        {
            using (var context = new Bookcaching_devEntities1())
            {
                context.ContextOptions.LazyLoadingEnabled = false;
                return context.Book.Where(b => b.ID == id).DefaultIfEmpty(null).First();
            }
        }

        public List<Book> GetBooksByDistance(double lat, double lon, double distance, List<int> alreadyLoaded = null)
        {
            if (alreadyLoaded == null) alreadyLoaded = new List<Int32>();
            using (var context = new Bookcaching_devEntities1())
            {
                try
                {
                    context.ContextOptions.LazyLoadingEnabled = false;
                    double latDiff = 2 * distance * LAT_M_DEG;
                    double lonDiff = 2 * distance * LON_M_DEG;

                    List<Book> booksSQL = context.Book.Where(b =>
                        (
                            b.Lat.HasValue && b.Lon.HasValue &&
                            Math.Abs(b.Lat.Value - lat) < latDiff && Math.Abs(b.Lon.Value - lon) < lonDiff)
                            && !alreadyLoaded.Contains(b.ID)
                        )
                        .ToList();
                    return booksSQL.Where(b => CalculateGreatCircleDistanceDeg(lat, lon, b.Lat.Value, b.Lon.Value) < distance).ToList();
                }
                catch (Exception e)
                {
                    // TODO error logging
                    return null;
                }

            }
        }

        public List<Book> GetBooksByArea(double latUL, double lonUL, double latBR, double lonBR, List<Int32> alreadyLoaded = null)
        {
            if (alreadyLoaded == null) alreadyLoaded = new List<Int32>();
            double maxLat = Math.Max(latBR, latUL);
            double minLat = Math.Min(latBR, latUL);
            double maxLon = Math.Max(lonBR, lonUL);
            double minLon = Math.Min(lonBR, lonUL);
            using (var context = new Bookcaching_devEntities1())
            {
                context.ContextOptions.LazyLoadingEnabled = false;
                List<Book> books = context.Book.Where(b =>
                        (
                            b.Lat.HasValue && b.Lon.HasValue &&
                            b.Lat.Value < maxLat && b.Lat.Value > minLat &&
                            b.Lon.Value < maxLon && b.Lon.Value > minLon &&
                            !alreadyLoaded.Contains(b.ID)
                        )
                    ).ToList();
                return books;
            }
        }

        public List<Book> GetBorrowedBooks(int userId)
        {
            using (var context = new Bookcaching_devEntities1())
            {
                context.ContextOptions.LazyLoadingEnabled = false;
                return context.Book.Where(b => b.CurrentUserRef == userId).ToList();
            }
        }

        public List<Book> GetUserBooks(int userId)
        {
            using (var context = new Bookcaching_devEntities1())
            {
                context.ContextOptions.LazyLoadingEnabled = false;
                List<Book> ret = context.Book.Where(b => b.OwnerRef == userId).ToList();
                return ret;
            }
        }
        /// <summary>
        /// Gets list of all books user found
        /// </summary>
        /// <param name="userId">ID of user</param>
        /// <returns>List of all books user found</returns>
        public List<Book> GetFoundBooks(int userId)
        {
            using (var context = new Bookcaching_devEntities1())
            {
                context.ContextOptions.LazyLoadingEnabled = false;
                List<int> foundEvents = context.Event.Where(e => e.UserRef == userId && e.EventTypeRef == 2).Select(e => e.BookRef).ToList();
                List<Book> ret = context.Book.Where(b => foundEvents.Contains(b.ID)).ToList();
                return ret;
            }
        }

        /// <summary>
        /// Creates event of returning book and sets bookstate, automaticky inserts user id of the user who borrowed the book, sets book new location
        /// </summary>
        /// <param name="bookId">ID of book</param>
        /// <param name="placeId">ID of place - null if book is not returned to known place</param>
        /// <param name="lat">Latitude, null of placeID used</param>
        /// <param name="lon">Longitude, null of placeID used</param>
        /// <param name="description">Optional event description, use null if not specified</param>
        /// <returns>Any error that occured, null if everything OK</returns>
        public string ReturnBook(int bookId, int? placeId, float? lat, float? lon, string description)
        {
            using (var context = new Bookcaching_devEntities1())
            {
                Event newEvent = new Event();
                if (context.Event.Count() > 0)
                    newEvent.ID = context.Event.Max(e => e.ID) + 1;


                newEvent.Time = DateTime.Now;
                newEvent.EventTypeRef = 3;

                // check if book exists
                Book book = context.Book.Where(b => b.ID == bookId).DefaultIfEmpty(null).First();
                if (book == null)
                {
                    return String.Format("Book {0} not found", bookId);
                }
                newEvent.BookRef = bookId;

                // set user and check book state at the same time
                if (book.BookState == 1)
                { // new book
                    newEvent.UserRef = book.OwnerRef;
                }
                else if (book.BookState == 3)
                {
                    if (book.CurrentUserRef.HasValue)
                    {
                        newEvent.UserRef = book.CurrentUserRef.Value;
                    }
                    else
                    {
                        return String.Format("Book {0} does not contain information about actual user", bookId);
                    }
                }
                else
                {
                    return String.Format("Book {0} not ready for return", bookId);
                }

                // set location
                if (placeId == null)
                {
                    newEvent.Lat = lat;
                    newEvent.Lon = lon;
                }
                else
                {
                    if (!context.Places.Any(p => p.ID == placeId.Value))
                    {
                        return String.Format("Place {0} specified but not found", placeId.Value);
                    }
                    else
                    {
                        Places p = context.Places.First(pl => pl.ID == placeId.Value);
                        newEvent.Lat = p.Lat;
                        newEvent.Lon = p.Lon;
                        book.PlaceRef = p.ID;
                    }
                }
                // insert event
                context.Event.AddObject(newEvent);

                // set borrowed user and book state
                book.BookState = 2;
                book.CurrentUserRef = null;

                // set location to book too
                book.Lat = newEvent.Lat;
                book.Lon = newEvent.Lon;

                // save
                try
                {
                    context.SaveChanges();
                }
                catch (Exception e)
                {
                    return e.ToString();
                }
                return null;
            }
        }

        /// <summary>
        /// Creates a borrow event of the book, sets user who borrowed it and bookstate, resets books lat nad lon
        /// </summary>
        /// <param name="bookId">Book ID</param>
        /// <param name="userId">User ID</param>
        /// <returns>Any error that occured, null if everything OK</returns>
        public string BorrowBook(int bookId, int userId)
        {
            using (var context = new Bookcaching_devEntities1())
            {
                Event newEvent = new Event();
                if (context.Event.Count() > 0)
                    newEvent.ID = context.Event.Max(e => e.ID) + 1;

                newEvent.Time = DateTime.Now;
                newEvent.EventTypeRef = 2;

                // check if book exists
                Book book = context.Book.Where(b => b.ID == bookId).DefaultIfEmpty(null).First();
                if (book == null)
                {
                    return String.Format("Book {0} not found", bookId);
                }
                newEvent.BookRef = bookId;

                // check the book state
                if (book.BookState == 1 || book.BookState == 3)
                {    // new or currently borrowed
                    return String.Format("Book {0} not ready for borrow", bookId);
                }

                // set if user exists
                User user = context.User.Where(u => u.ID == userId).DefaultIfEmpty(null).First();
                if (user == null)
                { // new book
                    return String.Format("User {0} not found", userId);
                }
                else
                {
                    newEvent.UserRef = user.ID;
                    // add counter
                    user.Borrowed += 1;
                }

                // set location
                newEvent.Lat = book.Lat;
                newEvent.Lon = book.Lon;
                // reset placeId if set
                book.PlaceRef = null;

                // insert event
                context.Event.AddObject(newEvent);

                // set borrowed user and book state
                book.BookState = 3;
                book.CurrentUserRef = userId;

                // null book location
                book.Lat = null;
                book.Lon = null;

                // save
                try
                {
                    context.SaveChanges();
                }
                catch (Exception e)
                {
                    return e.ToString();
                }
                return null;
            }
        }


        /// <summary>
        /// Creates a find event of the book
        /// </summary>
        /// <param name="bookId">Book ID</param>
        /// <param name="userId">User ID</param>
        /// <returns>Any error that occured, null if everything OK</returns>
        public string FindBook(int bookId, int userId)
        {
            using (var context = new Bookcaching_devEntities1())
            {
                Event newEvent = new Event();
                if (context.Event.Count() > 0)
                    newEvent.ID = context.Event.Max(e => e.ID) + 1;

                newEvent.Time = DateTime.Now;
                newEvent.EventTypeRef = 1;

                // check if book exists
                Book book = context.Book.Where(b => b.ID == bookId).DefaultIfEmpty(null).First();
                if (book == null)
                {
                    return String.Format("Book {0} not found", bookId);
                }
                newEvent.BookRef = bookId;

                // check the book state
                if (book.BookState == 1 || book.BookState == 3)
                {    // new or currently borrowed
                    return String.Format("Book {0} is not in the world and could not have been found by user", bookId);
                }

                // set if user exists
                User user = context.User.Where(u => u.ID == userId).DefaultIfEmpty(null).First();
                if (user == null)
                { // new book
                    return String.Format("User {0} not found", userId);
                }
                else
                {
                    newEvent.UserRef = user.ID;
                    // add counter
                    user.Borrowed += 1;
                }

                // set location
                newEvent.Lat = book.Lat;
                newEvent.Lon = book.Lon;

                // insert event
                context.Event.AddObject(newEvent);

                // save
                try
                {
                    context.SaveChanges();
                }
                catch (Exception e)
                {
                    return e.ToString();
                }
                return null;
            }
        }

        #endregion

        #region Place
        public Places GetPlaceById(int id)
        {
            using (var context = new Bookcaching_devEntities1())
            {
                context.ContextOptions.LazyLoadingEnabled = false;
                return context.Places.Where(p => p.ID == id).DefaultIfEmpty(null).First();
            }
        }

        public List<Places> GetPlacesByDistance(double lat, double lon, double distance, List<int> alreadyLoaded = null)
        {
            if (alreadyLoaded == null) alreadyLoaded = new List<Int32>();
            using (var context = new Bookcaching_devEntities1())
            {
                context.ContextOptions.LazyLoadingEnabled = false;
                try
                {
                    context.ContextOptions.LazyLoadingEnabled = false;
                    double latDiff = 2 * distance * LAT_M_DEG;
                    double lonDiff = 2 * distance * LON_M_DEG;

                    List<Places> placesSQL = context.Places.Where(p =>
                        (
                            Math.Abs(p.Lat - lat) < latDiff && Math.Abs(p.Lon - lon) < lonDiff)
                            && !alreadyLoaded.Contains(p.ID)
                        )
                        .ToList();
                    return placesSQL.Where(p => CalculateGreatCircleDistanceDeg(lat, lon, p.Lat, p.Lon) < distance).ToList();
                }
                catch (Exception e)
                {
                    // TODO: Error logging
                    Places p = new Places();
                    p.Description = e.ToString();
                    return new List<Places>() { p };
                }
            }
        }

        public List<Places> GetPlacesByArea(double latUL, double lonUL, double latBR, double lonBR, List<Int32> alreadyLoaded = null)
        {
            if (alreadyLoaded == null) alreadyLoaded = new List<Int32>();
            double maxLat = Math.Max(latBR, latUL);
            double minLat = Math.Min(latBR, latUL);
            double maxLon = Math.Max(lonBR, lonUL);
            double minLon = Math.Min(lonBR, lonUL);
            using (var context = new Bookcaching_devEntities1())
            {
                context.ContextOptions.LazyLoadingEnabled = false;
                List<Places> places = context.Places.Where(p =>
                        (
                            p.Lat < maxLat && p.Lat > minLat &&
                            p.Lon < maxLon && p.Lon > minLon &&
                            !alreadyLoaded.Contains(p.ID)
                        )
                    ).ToList();
                return places;
            }
        }

        #endregion

        public string Test()
        {
            // User
            string ret = "";
            ret += "User: ";
            try
            {
                using (var c = new Bookcaching_devEntities1())
                {
                    User test = c.User.First();
                    ret += "OK " + test.ID;
                }
            }
            catch (Exception)
            {
                ret += "fail";
            }

            ret += ";  User.Name: ";
            try
            {
                using (var c = new Bookcaching_devEntities1())
                {
                    string s = c.User.Select(u => u.UserName).First();
                    ret += "OK " + s;
                }
            }
            catch (Exception)
            {
                ret += "fail";
            }

            ret += ";  Book: ";
            try
            {
                using (var c = new Bookcaching_devEntities1())
                {
                    Book b = c.Book.First();
                    ret += "OK " + b.ID;
                }
            }
            catch (Exception)
            {
                ret += "fail";
            }

            ret += ";  Book.BookState: ";
            try
            {
                using (var c = new Bookcaching_devEntities1())
                {
                    int i = c.Book.Select(b => b.BookState).First();
                    ret += "OK " + i;
                }
            }
            catch (Exception)
            {
                ret += "fail";
            }

            // test foundBooks
            ret += ";  GetFoundBooks(3): ";
            try
            {
                ret += GetFoundBooks(3).Count;
            }
            catch (Exception)
            {
                ret += "fail";
            }

            // test foundBooks for non-existing user
            ret += ";  GetFoundBooks(-1): ";
            try
            {
                ret += GetFoundBooks(-1).Count;
            }
            catch (Exception)
            {
                ret += "fail";
            }

            return ret;
        }
    }
}
